package bancosys.tec.persist.audit.diff;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.collections.MultiMap;
import org.apache.commons.collections.map.MultiValueMap;
import org.apache.commons.lang.StringUtils;
import org.hibernate.Criteria;
import org.hibernate.EntityMode;
import org.hibernate.Hibernate;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.Restrictions;
import org.hibernate.metadata.ClassMetadata;
import org.hibernate.persister.entity.AbstractEntityPersister;
import org.hibernate.type.Type;

import bancosys.tec.persist.Persistable;
import bancosys.tec.persist.audit.Auditable;
import bancosys.tec.persist.audit.domain.AuditProperty;
import bancosys.tec.persist.audit.domain.AuditValuedProperty;
import bancosys.tec.persist.audit.domain.AuditVersion;
import bancosys.tec.persist.authorization.AuthorizationContext;
import bancosys.tec.persist.authorization.DisplayNameUtil;
import bancosys.tec.persist.bussobj.AuthPersistableBusinessObject;
import bancosys.tec.persist.controller.PersistenceEnabledController;
import bancosys.tec.persist.hibernate.PartitionSessionFactory;
import bancosys.tec.persist.persister.DefaultPersister;
import bancosys.tec.persist.persister.PersistenceAction;

/**
 * @created 04/09/2007
 * @author Gustavo Almeida
 */
public class AuditDiff {

    private final SessionFactory sessionFactory;

    private final PersistenceEnabledController controller;

    private AuditVersion version;

    private final PersistenceAction action;

    private Collection<String> ingnoreList = new LinkedList<String>();

    private Auditable oldState;

    private Auditable newState;

    private int computedDiff;

    /**
     * Construtor.
     * 
     * @param action ação da persistência
     * @param controller controller de auditoria
     * @param sessionFactory session factory
     */
    public AuditDiff(SessionFactory sessionFactory, PersistenceEnabledController controller, PersistenceAction action) {
        this.controller = controller;
        this.sessionFactory = sessionFactory;
        this.action = action;
    }

    /**
     * Lista de propriedades que serão ignoradas pelo diff. Aceita regex.
     * 
     * @param ignoreList lista de regex.
     */
    public void setIngnoreList(Collection<String> ignoreList) {
        this.ingnoreList = ignoreList;
    }

    /**
     * Adiciona uma propriedade à lista de propriedades ignoradas.
     * 
     * @param ignoreRegex regex da propriedade.
     */
    public void addIgnoreList(String ignoreRegex) {
        this.ingnoreList.add(ignoreRegex);
    }

    /**
     * Compara o bean com oldBean. oldBean precisa estar associado a uma sessão hibernate.
     * 
     * @param newBean bean com estado novo.
     * @param oldBean bean com estado anterior.
     * @return resultado da comparação.
     * @deprecated usar a versao que manda o modification source - {@link #diff(Auditable, Auditable, String)}
     */
    @Deprecated
    public AuditVersion diff(Auditable oldBean, Auditable newBean) {
        return this.diff(oldBean, newBean, "unknown");
    }

    /**
     * Compara o bean com oldBean. oldBean precisa estar associado a uma sessão hibernate.
     * 
     * @param newBean bean com estado novo.
     * @param oldBean bean com estado anterior.
     * @param modificationSource the String
     * @return resultado da comparação.
     */
    public AuditVersion diff(Auditable oldBean, Auditable newBean, String modificationSource) {
        this.oldState = oldBean;
        this.newState = newBean;
        this.version = new AuditVersion();
        this.version.setAction(this.action);
        this.version.setModificationSource(modificationSource);
        this.version.setUser(this.nullSafeGetVersionUser(newBean, oldBean));
        this.computedDiff = this.diffRecursion("", oldBean, newBean);
        return this.version;
    }

    /**
     * Compara o bean com oldBean. oldBean precisa estar associado a uma sessão hibernate.
     * 
     * @param removedBeanUser nome do usuário responsável pela exclusão.
     * @param oldBean bean com estado anterior.
     * @param modificationSource the String
     * @return resultado da comparação.
     */
    public AuditVersion diff(Auditable oldBean, String removedBeanUser, String modificationSource) {
        this.diff(oldBean, (Auditable) null, modificationSource);
        this.version.setUser(removedBeanUser);
        return this.version;
    }
    
    /**
     * Compara o bean com sua última versão persistida.
     * 
     * @param clazz classe do bean.
     * @param oldBeanId id da versão antiga do bean.
     * @param newBean versão nova do bean.
     * @param modificationSource the modification source
     * @return resultado da comparação.
     */
    public AuditVersion diff(Class<?> clazz, Long oldBeanId, Auditable newBean, String modificationSource) {
        return this.diff(clazz, oldBeanId, newBean, null, modificationSource);
    }

    /**
     * Compara o bean com sua última versão persistida.
     * 
     * @param clazz classe do bean.
     * @param oldBeanId id da versão antiga do bean.
     * @param removedBeanUser nome do responsável pela exclusão do bean.
     * @param modificationSource the modification source
     * @return resultado da comparação.
     */
    public AuditVersion diff(Class<? extends Persistable> clazz, Long oldBeanId, String removedBeanUser, String modificationSource) {
        return this.diff(clazz, oldBeanId, null, removedBeanUser, modificationSource);
    }

    /**
     * Compara o bean com sua última versão persistida.
     * 
     * @param clazz classe do bean.
     * @param oldBeanId id da versão antiga do bean.
     * @param removedBeanUser nome do responsável pela exclusão do bean.
     * @param modificationSource the modification source
     * @return resultado da comparação.
     */
    private AuditVersion diff(Class<?> clazz, Long oldBeanId, Auditable newBean, String removedBeanUser, String modificationSource) {
        AuthorizationContext oldCtx = null;
        try {
            if (this.sessionFactory instanceof PartitionSessionFactory) {
                PartitionSessionFactory partitionSessionFactory = (PartitionSessionFactory) this.sessionFactory;
                oldCtx = partitionSessionFactory.getThreadPartition();
                if (PersistenceAction.coldInsert.equals(this.action)) {
                    partitionSessionFactory.setThreadPartition(AuthorizationContext.cold);
                } else {
                    partitionSessionFactory.setThreadPartition(AuthorizationContext.hot);
                }
            }
            Auditable oldBean = this.loadBean(this.sessionFactory.getCurrentSession(), clazz, oldBeanId);
            if (newBean != null) {
                return this.diff(oldBean, newBean, modificationSource);
            } else {
                return this.diff(oldBean, removedBeanUser, modificationSource);
            }
        } finally {
            if (oldCtx != null) {
                ((PartitionSessionFactory) this.sessionFactory).setThreadPartition(oldCtx);
            }
        }
    }
    
    
    /**
     * Compara o bean com sua última versão persistida.
     * 
     * @param clazz classe do bean.
     * @param oldBeanId id da versão antiga do bean.
     * @param newBean versão nova do bean.
     * @return resultado da comparação.
     * @deprecated usar {@link #diff(Class, Long, Auditable, String)}
     */
    @Deprecated
    public AuditVersion diff(Class<?> clazz, Long oldBeanId, Auditable newBean) {
        return this.diff(clazz, oldBeanId, newBean, "unknown");
    }

    /**
     * Compara o bean com sua última versão persistida.
     * 
     * @param oldBean versão antiga do bean.
     * @param clazz classe do bean
     * @param newBeanId id da versão nova do bean.
     * @return resultado da comparação.
     */
    public AuditVersion diff(Auditable oldBean, Class<?> clazz, Long newBeanId) {
        AuthorizationContext oldCtx = null;
        try {
            if (this.sessionFactory instanceof PartitionSessionFactory) {
                PartitionSessionFactory partitionSessionFactory = (PartitionSessionFactory) this.sessionFactory;
                oldCtx = partitionSessionFactory.getThreadPartition();
                if (PersistenceAction.coldInsert.equals(this.action)) {
                    partitionSessionFactory.setThreadPartition(AuthorizationContext.cold);
                } else {
                    partitionSessionFactory.setThreadPartition(AuthorizationContext.hot);
                }
            }
            Auditable newBean = this.loadBean(this.sessionFactory.getCurrentSession(), clazz, newBeanId);
            return this.diff(oldBean, newBean);
        } finally {
            if (oldCtx != null) {
                ((PartitionSessionFactory) this.sessionFactory).setThreadPartition(oldCtx);
            }
        }
    }

    /**
     * Carrega o bean da base, devolve <code>null</code> se nao for encontrado
     * 
     * @param session session
     * @param clazz classe do bean
     * @param id id do bean
     * @return bean ou <code>null</code> caso nao seja encontrado.
     */
    private Auditable loadBean(Session session, Class<?> clazz, Serializable id) {
        Criteria crit = session.createCriteria(clazz);
        crit.add(Restrictions.idEq(id));
        Collection<?> l = crit.list();
        if (l.size() == 0) {
            return null;
        } else {
            return (Auditable) l.iterator().next();
        }
    }

    /**
     * Devolve o estado anterior do bean.
     * 
     * @return o estado anterior do bean.
     */
    public Auditable getOldState() {
        return this.oldState;
    }

    /**
     * Devolve o estado atual do bean.
     * 
     * @return o estado atual do bean.
     */
    public Auditable getNewState() {
        return this.newState;
    }

    /**
     * Recursão principal de comparação.
     * 
     * @param previewsXpath caminho para os objetos.
     * @param oldObject objeto antigo.
     * @param newObject objeto novo.
     * @param ignoreNonInitialized
     * @return o número de propriedades diferentes da versão.
     */
    private int diffRecursion(String previewsXpath, Object oldObject, Object newObject) {
        if (oldObject == null && newObject == null) {
            // Trata-se de uma remoção de um dado que não está presente na base.
            // Nesse caso, não deve haver auditoria.
            return 0;
        }
        if (!Hibernate.isInitialized(newObject)) {
            return 0;
        }
        int diffs = 0;

        ClassMetadata cm = this.getMetadata(oldObject, newObject);
        @SuppressWarnings("unchecked")
        Class<? extends Persistable> entityClass = cm.getMappedClass(EntityMode.POJO);

        // properties
        Type[] types = cm.getPropertyTypes();
        String[] properties = cm.getPropertyNames();
        Object[] oldValues = this.nullSafeGetValues(cm, oldObject, properties.length);
        Object[] newValues = this.nullSafeGetValues(cm, newObject, properties.length);

        for (int i = 0; i < types.length; i++) {

            Object oldValue = oldValues[i];
            Object newValue = newValues[i];
            Type type = types[i];
            String xpath = this.concatXpath(previewsXpath, properties[i]);
            String name = DisplayNameUtil.getBeanStrictPropertyDisplayName(entityClass, properties[i]);

            if (oldValue == null && newValue == null) {
                if (name != null) {
                    AuditProperty property = this.createValuedProperty(xpath, name, oldValue, newValue);
                    this.version.addProperty(property);
                }
                continue;
            }

            if (this.ignore(xpath)) {
                continue;
            }

            if (!type.isAssociationType()) {
                diffs += this.diffNonAssociativeProperty(xpath, name, oldValue, newValue);
            } else if (DefaultPersister.isCascadeSave(((AbstractEntityPersister) cm).getCascadeStyle(i))) {
                if (type.isCollectionType()) {
                    diffs += this.diffCascadeCollectionType(oldValue, newValue, type, xpath, name, entityClass);
                } else if (Auditable.class.isAssignableFrom(types[i].getReturnedClass())) {
                    // se o cara é auditavel percorrer as propriedades dele
                    diffs += this.diffCascadeProperty(xpath, name, (Auditable) oldValue, (Auditable) newValue);
                } else {
                    // se nao for auditavel tratar como se nao fosse "cascade"
                    diffs += this.diffNonCascadeProperty(xpath, name, oldValue, newValue);
                }
            } else {
                if (type.isCollectionType()) {
                    diffs += this.diffNonCascadeCollectionType(oldValue, newValue, type, xpath, name);
                } else {
                    diffs += this.diffNonCascadeProperty(xpath, name, oldValue, newValue);
                }
            }
        }

        return diffs;
    }

    /**
     * Executa a recursão em propriedades cascade <code>CollectionType</code> do hibernate, pode ser <code>Collection</code> ou
     * <code>Map</code>
     * 
     * @param oldValue valor anterior
     * @param newValue valor atual
     * @param type tipo
     * @param xpath path
     * @param name nome
     * @param entityClass Class
     * @return o número de propriedades diferentes da coleção.
     */
    @SuppressWarnings("unchecked")
    private int diffCascadeCollectionType(Object oldValue, Object newValue, Type type, String xpath, String name,
            Class<? extends Persistable> entityClass) {
        if (Map.class.isAssignableFrom(type.getReturnedClass())) {
            return this.diffCascadeMap(xpath, name, (Map) oldValue, (Map) newValue);
        } else {
            return this.diffCascadeCollection(xpath, name, (Collection) oldValue, (Collection) newValue, entityClass);
        }
    }

    /**
     * Executa a recursão em propriedades não-cascade <code>CollectionType</code> do hibernate, pode ser <code>Collection</code> ou
     * <code>Map</code>
     * 
     * @param oldValue valor anterior
     * @param newValue valor atual
     * @param type tipo
     * @param xpath path
     * @param name nome
     * @return o número de propriedades diferentes da coleção.
     */
    @SuppressWarnings("unchecked")
    private int diffNonCascadeCollectionType(Object oldValue, Object newValue, Type type, String xpath, String name) {
        if (Map.class.isAssignableFrom(type.getReturnedClass())) {
            return this.diffNonCascadeMap(xpath, name, (Map) oldValue, (Map) newValue);
        } else {
            return this.diffNonCascadeCollection(xpath, name, (Collection) oldValue, (Collection) newValue);
        }
    }

    /**
     * Devolve o hibernate metadata do auditavel. Se o objeto novo for <code>null</code> (remoção), utiliza o objeto antigo como base. Caso
     * contrário (inserção ou alteração) utiliza o objeto novo.
     * 
     * @param oldObject objeto antigo
     * @param newObject objeto atual
     * @return metadata
     */
    private ClassMetadata getMetadata(Object oldObject, Object newObject) {
        ClassMetadata cm;
        if (newObject != null) {
            cm = this.getMetadata(newObject);
        } else {
            cm = this.getMetadata(oldObject);
        }
        return cm;
    }

    /**
     * Dado um xpath para uma propriedade informa se ela deve ser ignorada ou não.
     * 
     * @param xpath caminho para a propriedade
     * @return <code>true</code> caso deva ser ignorada, <code>false</code> caso contrário.
     */
    protected boolean ignore(String xpath) {
        for (String ignoreRegex : this.ingnoreList) {
            if (xpath.matches(ignoreRegex)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Compara duas propriedades não associativas.
     * 
     * @param xpath path.
     * @param name nome da property
     * @param oldValue valor antigo.
     * @param newValue valor atual.
     * @return o número de propriedades diferentes.
     */
    private int diffNonAssociativeProperty(String xpath, String name, Object oldValue, Object newValue) {
        if (name != null) {
            AuditProperty property = this.createValuedProperty(xpath, name, oldValue, newValue);
            this.version.addProperty(property);
        }
        return !this.nullSafeEquals(oldValue, newValue) ? 1 : 0;
    }

    /**
     * Cria uma nova propriedade valorada de auditoria.
     * 
     * @param xpath path.
     * @param name nome da property.
     * @param oldValue valor antigo.
     * @param newValue valor atual.
     * @return AuditProperty
     */
    private AuditValuedProperty createValuedProperty(String xpath, String name, Object oldValue, Object newValue) {
        AuditValuedProperty property = new AuditValuedProperty();
        property.setXpath(xpath);
        property.setOldValue(oldValue);
        property.setNewValue(newValue);
        property.setName(name);
        return property;
    }

    /**
     * Cria uma nova propriedade de auditoria.
     * 
     * @param xpath path.
     * @param name nome da property.
     * @return AuditProperty
     */
    private AuditProperty createProperty(String xpath, String name) {
        AuditProperty property = new AuditProperty();
        property.setXpath(xpath);
        property.setName(name);
        return property;
    }

    /**
     * Compara duas propriedades de associação sem cascade.
     * 
     * @param xpath path.
     * @param name nome da property.
     * @param oldValue valor antigo.
     * @param newValue valor atual.
     * @return o número de propriedades diferentes.
     */
    private int diffNonCascadeProperty(String xpath, String name, Object oldValue, Object newValue) {
        boolean diff = !this.nullSafeToStringEquals(oldValue, newValue);
        if (diff || name != null) {
            this.version.addProperty(this.createValuedProperty(xpath, name, oldValue, newValue));
        }
        return diff ? 1 : 0;
    }

    /**
     * Compara duas propriedades de associação com cascade.
     * 
     * @param xpath path.
     * @param name nome da property.
     * @param oldValue valor antigo.
     * @param newValue valor atual.
     * @return o número de propriedades diferentes.
     */
    private int diffCascadeProperty(String xpath, String name, Auditable oldValue, Auditable newValue) {
        int diffs = this.diffRecursion(xpath, oldValue, newValue);
        if (diffs > 0) {
            this.version.addProperty(this.createProperty(xpath, name));
        }
        return diffs;
    }

    /**
     * Compara dois mapas com cascade.
     * 
     * @param xpath path.
     * @param name nome da property.
     * @param oldValue valor antigo.
     * @param newValue valor atual.
     * @return o número de propriedades diferentes.
     */
    private int diffCascadeMap(String xpath, String name, Map<?, ?> oldValue, Map<?, ?> newValue) {
        int diffs = 0;

        for (Object key : this.nullSafeGetKeys(oldValue, newValue)) {
            Object oldItem = (oldValue == null) ? null : oldValue.get(key);
            Object newItem = (newValue == null) ? null : newValue.get(key);
            diffs += this.diffRecursion(this.mapXpath(xpath, key.toString()), oldItem, newItem);
        }

        if (diffs > 0) {
            this.version.addProperty(this.createProperty(xpath, name));
        }

        return diffs;
    }

    /**
     * Compara dois mapas sem cascade.
     * 
     * @param xpath path.
     * @param name nome da property.
     * @param oldValue valor antigo.
     * @param newValue valor atual.
     * @return o número de propriedades diferentes.
     */
    private int diffNonCascadeMap(String xpath, String name, Map<?, ?> oldValue, Map<?, ?> newValue) {
        int diffs = 0;

        for (Object key : this.nullSafeGetKeys(oldValue, newValue)) {
            Object oldItem = (oldValue == null) ? null : oldValue.get(key);
            Object newItem = (newValue == null) ? null : newValue.get(key);
            diffs += this.diffNonCascadeProperty(this.mapXpath(xpath, key.toString()), name, oldItem, newItem);
        }

        if (diffs > 0) {
            this.version.addProperty(this.createProperty(xpath, name));
        }

        return diffs;
    }

    /**
     * Cria path para itens dentro de um mapa.
     * 
     * @param map path do mapa.
     * @param key a chave do item no mapa.
     * @return path para o item do mapa.
     */
    private String mapXpath(String map, String key) {
        StringBuilder sb = new StringBuilder(map);
        sb.append("[").append(key).append("]");
        return sb.toString();
    }

    /**
     * Devolve um conjunto com todas as chaves dos dois mapas, ou um conjunto vazio se ambos forem null.
     * 
     * @param map1 um mapa
     * @param map2 um mapa
     * @return um conjunto de todas as chaves dos dois mapas.
     */
    private Set<Object> nullSafeGetKeys(Map<?, ?> map1, Map<?, ?> map2) {
        Set<Object> keys = new HashSet<Object>();

        if (map1 != null) {
            keys.addAll(map1.keySet());
        }

        if (map2 != null) {
            keys.addAll(map2.keySet());
        }
        return keys;
    }

    /**
     * Compara duas coleções associadas com cascade.
     * 
     * @param xpath path.
     * @param name property name.
     * @param oldValue valor antigo.
     * @param newValue valor atual.
     * @param entityClass Class
     * @return o número de propriedades diferentes das coleções.
     */
    private int diffCascadeCollection(String xpath, String name, Collection<Object> oldValue, Collection<Object> newValue,
            Class<? extends Persistable> entityClass) {
        Collection<Object> l1 = new LinkedList<Object>();
        Collection<Object> l2 = new LinkedList<Object>();

        if (oldValue != null) {
            l1.addAll(oldValue);
        }
        if (newValue != null) {
            l2.addAll(newValue);
        }

        this.removeNullElements(l1);
        this.removeNullElements(l2);

        int diffs = this.diffCascadeCollectionIteration(xpath, l1, l2, entityClass);
        if (diffs > 0) {
            this.version.addProperty(this.createProperty(xpath, name));
        }
        return diffs;
    }

    /**
     * Percorre duas collections com cascade gerandos os diffs dos elementos.
     * 
     * @param xpath path
     * @param l1 collection maior
     * @param l2 collection menor
     * @param entityClass Class
     * @return o número de propriedades diferentes nas coleções.
     */
    private int diffCascadeCollectionIteration(String xpath, Collection<Object> l1, Collection<Object> l2,
            Class<? extends Persistable> entityClass) {
        MultiMap map = this.mapCollectionObjects(xpath, l2);
        int i = 0;
        int diffs = 0;

        String nome = DisplayNameUtil.getBeanPropertyDisplayName(entityClass, xpath);
        if (nome == null) {
            nome = xpath;
        }

        for (Object p1 : l1) {
            Serializable key = this.getKey(p1);
            List<Object> toRemove = new ArrayList<Object>();
            Collection<?> col = (Collection<?>) map.get(key);

            if (col == null) {
                diffs += this.diffRecursion(this.listXpath(nome, i), p1, null);
            } else {
                for (Object obj : col) {
                    diffs += this.diffRecursion(this.listXpath(nome, i), p1, obj);
                    toRemove.add(obj);
                }

                for (Object obj : toRemove) {
                    map.remove(key, obj);
                }
            }
            i++;
        }

        if (map.values().size() > 0) {
            for (Object p2 : map.values()) {
                diffs += this.diffRecursion(this.listXpath(nome, i), null, p2);
                i++;
            }
        }
        return diffs;
    }

    /**
     * Devolve a chave a ser usada no mapa de objetos da coleção.
     * 
     * @param p1 o objeto do mapa.
     * @return a chave a ser usada no mapa de objetos da coleção.
     */
    private Serializable getKey(Object p1) {
        Serializable key;
        if (p1 instanceof Persistable) {
            key = ((Persistable) p1).getPk();

            if (p1 instanceof AuthPersistableBusinessObject) {
                AuthPersistableBusinessObject p = (AuthPersistableBusinessObject) p1;
                if (p.getAuthId() != null) {
                    key = p.getAuthId();
                }
            }
        } else { // trata-se de um embeddable
            key = p1.hashCode();
        }
        return key;
    }

    /**
     * Remove elementos <code>null</code> da lista.
     * 
     * @param list lista.
     */
    private void removeNullElements(Collection<Object> list) {
        Iterator<?> it = list.iterator();
        while (it.hasNext()) {
            if (it.next() == null) {
                it.remove();
            }
        }
    }

    /**
     * Compara duas coleções em associações sem cascade.
     * 
     * @param xpath path.
     * @param name nome da property.
     * @param oldValue valor antigo.
     * @param newValue valor atual.
     * @return o número de propriedades diferentes das coleções.
     */
    private int diffNonCascadeCollection(String xpath, String name, Collection<Object> oldValue, Collection<Object> newValue) {
        Collection<Object> l1 = new LinkedList<Object>();
        Collection<Object> l2 = new LinkedList<Object>();

        if (oldValue != null) {
            l1.addAll(oldValue);
        }
        if (newValue != null) {
            l2.addAll(newValue);
        }

        return this.diffNonCascadeCollectionIteration(xpath, name, l1, l2);
    }

    /**
     * Percorre duas collections sem cascade gerandos os diffs dos elementos.
     * 
     * @param name nome da collection
     * @param xpath path
     * @param l1 collection maior
     * @param l2 collection menor
     * @return o número de propriedades diferentes nas coleções.
     */
    private int diffNonCascadeCollectionIteration(String xpath, String name, Collection<Object> l1, Collection<Object> l2) {
        MultiValueMap map = this.mapCollectionObjects(xpath, l2);
        int i = 0;
        int diffs = 0;

        for (Object p1 : l1) {
            Serializable key = this.getKey(p1);
            List<Object> toRemove = new ArrayList<Object>();
            Collection<?> col = (Collection<?>) map.get(key);
            if (col == null) {
                diffs += this.diffNonCascadeProperty(this.listXpath(xpath, i), name, p1, null);
            } else {
                for (Object obj : col) {
                    diffs += this.diffNonCascadeProperty(this.listXpath(xpath, i), name, p1, obj);
                    toRemove.add(obj);
                }

                for (Object obj : toRemove) {
                    map.remove(key, obj);
                }
            }
            i++;
        }

        if (map.values().size() > 0) {
            for (Object p2 : map.values()) {
                diffs += this.diffNonCascadeProperty(this.listXpath(xpath, i), name, null, p2);
                i++;
            }
        }

        return diffs;
    }

    // -----------------------------------

    /**
     * Compara os objetos nos caso de um ou ambos serem <code>null</code> e executa "<code>o1.equals(o2)</code>" caso ambos sejam diferentes
     * de <code>null</code>.
     * 
     * @param o1 primeiro objeto da comparação.
     * @param o2 segundo objeto da comparação.
     * @return <code>true</code> caso o1 seja igual a o2, <code>false</code> caso contrário.
     */
    private boolean nullSafeEquals(Object o1, Object o2) {
        if (o1 == null && o2 == null) {
            return true;
        }
        if (o1 == null) {
            return false;
        }
        return o1.equals(o2);
    }

    /**
     * Compara a representação string de dois objetos.
     * 
     * @param o1 primeiro objeto da comparação.
     * @param o2 segundo objeto da comparação.
     * @return <code>true</code> caso a representação <code>String</code> de o1 seja igual a de o2, <code>false</code> caso contrário.
     */
    protected boolean nullSafeToStringEquals(Object o1, Object o2) {
        if (o1 == null && o2 == null) {
            return true;
        }

        if (o1 == null || o2 == null) {
            return false;
        }

        String s1 = o1.toString();
        String s2 = o2.toString();

        if (s1 == null && s2 == null) {
            return true;
        }

        if (s1 == null || s2 == null) {
            return false;
        }
        return s1.equals(s2);
    }

    /**
     * Devolve o metadata da classe do objeto.
     * 
     * @param object objeto.
     * @return o metadata da classe do objeto.
     */
    private ClassMetadata getMetadata(Object object) {
        return this.sessionFactory.getClassMetadata(this.getRealClass(object.getClass()));
    }

    /**
     * Devolve a classe representada por <code>clazz</code> caso seja um proxy do hibernate, devolve <code>clazz</code> caso contrário.
     * 
     * @param clazz classe.
     * @return a classe representada por <code>clazz</code> caso seja um proxy do hibernate, devolve <code>clazz</code> caso contrário.
     */
    private Class<?> getRealClass(Class<?> clazz) {
        if (clazz.getName().indexOf("$$") > 0) {
            return clazz.getSuperclass();
        } else {
            return clazz;
        }
    }

    /**
     * Dada uma lista de objetos persistíveis devolve um mapa dos objetos.
     * 
     * @param xpath path.
     * @param c coleção de objetos persistíveis.
     * @return mapa com os objetos indexados pelo id.
     */
    private MultiValueMap mapCollectionObjects(String xpath, Collection<Object> c) {
        MultiValueMap map = new MultiValueMap();
        int id = 0;

        for (Object obj : c) {
            if (obj instanceof Persistable) {
                Persistable persistable = (Persistable) obj;
                Serializable pk = persistable.getPk();
                if (obj instanceof AuthPersistableBusinessObject) {
                    AuthPersistableBusinessObject p = (AuthPersistableBusinessObject) obj;
                    if (p.getAuthId() != null) {
                        pk = p.getAuthId();
                    }
                }
                if (pk == null) {
                    pk = persistable.getClass().getName() + id++;
                }
                map.put(pk, persistable);
            } else { // trata-se de um embeddable
                map.put(obj.hashCode(), obj);
            }
        }
        return map;
    }

    /**
     * Concatena path.
     * 
     * @param xpath1 primeira parte.
     * @param xpath2 segunda parte.
     * @return path concatenado.
     */
    private String concatXpath(String xpath1, String xpath2) {
        if (xpath1 == null || xpath1.length() < 1) {
            return xpath2;
        }
        StringBuilder sb = new StringBuilder(xpath1);
        sb.append(".");
        sb.append(xpath2);
        return sb.toString();
    }

    /**
     * Cria path para itens dentro de uma lista.
     * 
     * @param list path da lista.
     * @param position posição do item na lista.
     * @return path para o item da lista.
     */
    private String listXpath(String list, int position) {
        StringBuilder sb = new StringBuilder(list);
        sb.append("[").append(position).append("]");
        return sb.toString();
    }

    /**
     * Devolve os valores das propriedades persistíveis do objeto, devolve um array de <code>null</code>s caso o objeto seja
     * <code>null</code>.
     * 
     * @param cm metadata da classe do objeto.
     * @param obj objeto
     * @param size quantidade de propriedades persistíveis do objeto.
     * @return um array com os valores das propriedades persistíveis do objeto ou um array de <code>null</code>s caso o objeto seja
     * <code>null</code>.
     */
    private Object[] nullSafeGetValues(ClassMetadata cm, Object obj, int size) {
        if (obj == null) {
            return new Object[size];
        }
        return cm.getPropertyValues(obj, EntityMode.POJO);
    }

    /**
     * Devolve o nome do usuário atual da thread.
     * 
     * @param newBean o auditável novo, usado em inserções e updates.
     * @param oldBean o auditável antigo, usado em remoções.
     * @return nome do usuário atual.
     */
    private String nullSafeGetVersionUser(Auditable newBean, Auditable oldBean) {
        if (this.isNewVersionWithAuthorization(newBean)) {
            return newBean.getUser();
        } else if (this.isRemovalWithAuthorization(newBean, oldBean)) {
            return oldBean.getUser();
        } else { // No authorization involved
            return this.controller.getSecurityService().getCurrentUser();
        }
    }

    /**
     * Checks if this is a new version that was authorized
     * 
     * @param newBean newBean
     * @return boolean
     */
    private boolean isNewVersionWithAuthorization(Auditable newBean) {
        return newBean != null && !StringUtils.isBlank(newBean.getUser());
    }

    /**
     * Checks if this is a removal with authorization
     * 
     * @param newBean newBean
     * @param oldBean oldBean
     * @return boolean
     */
    private boolean isRemovalWithAuthorization(Auditable newBean, Auditable oldBean) {
        return newBean == null && oldBean != null && !StringUtils.isBlank(oldBean.getUser());
    }

    /**
     * @return the computedDiff
     */
    public int getComputedDiff() {
        return this.computedDiff;
    }
}
